iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 10
0

本文同步分享於個人blog

在昨天終於把所有的Design Principle給講完了,今天開始進入Design Pattern。首先是創建型模型,其中單例模式為最常見的模式之一。讓我們開始了解什麼是單例模式。

定義


Singleton = one instance ONLY

相信大家只要查過Singleton Pattern,就會出現這一句話。那到底one instance ONLY是什麼意思呢?

我們用一個簡單的問題來了解!!

問題:超級市場與送貨員

送貨員要從送貨進超市,或是要從超市取貨送給客戶,我們可以用以下的程式碼實作這個情境。

public class NotSinglePattern {
    public static void main(String args[]) {
        Supermarket mSupermarket1 = new Supermarket();
        Supermarket mSupermarket2 = new Supermarket();
        Freight freight1 = new Freight(mSupermarket1);
        Freight freight2 = new Freight(mSupermarket2);

        System.out.print("同一家超市?");

        if(mSupermarket1.equals(mSupermarket2)){
            System.out.println("yes");
        }else {
            System.out.println("no");
        }
        
        freight1.moveIn(30);
        System.out.println("freight1搬完後商品數量:"+freight1.mSupermarket.getQuantity());
        freight2.moveOut(50);
        System.out.println("freight2搬完後商品數量:"+freight2.mSupermarket.getQuantity());
    }
}


class Supermarket {
    private int quantity = 100;
    public Supermarket() {
    }
    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }
    public int getQuantity() {
        return quantity;
    }
}

class Freight{
    public Supermarket mSupermarket;
    public Freight(Supermarket supermarket){
        mSupermarket = supermarket;
    }
    public void moveIn(int i){
        mSupermarket.setQuantity(mSupermarket.getQuantity()+i);
    }
    public void moveOut(int i){
        mSupermarket.setQuantity(mSupermarket.getQuantity()-i);
    }
}

output:

同一家超市?no
freight1搬完後商品數量:130
freight2搬完後商品數量:50

由上述程式碼的來看,一號送貨員要送進30件貨進超市,二號送貨員要從超市拿50件貨送給客戶,超商原本的貨物量有100件。所以一號送完貨物變130件,二號取完貨物應該變80件,但結果卻是50。而在結果第一行顯示兩個送貨員分別去到不同的市場,所以出現了問題。但我們希望他是到同一個超市送貨或取貨,所以我們修改一下程式:

public class SinglePattern {
    public static void main(String args[]) {
        Supermarket mSupermarket1 = Supermarket.getInstance();
        Supermarket mSupermarket2 = Supermarket.getInstance();
        Freight freight1 = new Freight(mSupermarket1);
        Freight freight2 = new Freight(mSupermarket2);

        System.out.print("同一家超市?");

        if(mSupermarket1.equals(mSupermarket2)){
            System.out.println("yes");
        }else {
            System.out.println("no");
        }
        
        freight1.moveIn(30);
        System.out.println("freight1搬完後商品數量:"+freight1.mSupermarket.getQuantity());
        freight2.moveOut(50);
        System.out.println("freight2搬完後商品數量:"+freight2.mSupermarket.getQuantity());
    }
}


class Supermarket {
    private int quantity = 100;
    private static Supermarket uniqueInstance  = new Supermarket();
    public static Supermarket getInstance() {
        return uniqueInstance;
    }
    private Supermarket() {
    }
    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }
    public int getQuantity() {
        return quantity;
    }
}


output: :

同一家超市?yes
freight1搬完後商品數量:130
freight2搬完後商品數量:80

在一開始就把超市的實體建立起來,如此一來就只會有一間超市,送貨員就可以在同一個超市送貨或取貨了!!


在程式開發上,常常也會遇到需要共用同一個物件的問題,如同第一篇提到,Design Pattern是為了解決重複出現的問題而衍生出來的解決方案。
而Singleton便是今天這個案例的解決方案!!而Singleton Pattern在不同的情境也會有不同的方法。

Singleton Pattern 的種類


  • Greed Singleton

class Supermarket {
    private int quantity = 100;
    private static Supermarket uniqueInstance  = new Supermarket();
    private Supermarket() {
    }
    public static Supermarket getInstance() {
        return uniqueInstance;
    }
    // ...略
}

將原本的constructor宣告成private,取而代之的是開放一個getInstance()的方法,供外部使用此類別。這樣的好處在於,如果要重複使用這個class,不會每次都有一個新的物件產生,原本的物件可以重複使用。

不過因為static的關係,在program啟動之後就會在memory裡面存了這個實體物件,但這個物件不一定時常被存取,不需要一開始就準備好這個實體物件,所以可以把程式改成lazy Initialization

  • Lazy Initialization

class Supermarket {
    private int quantity = 100;
    private static Supermarket uniqueInstance  = null;
    private Supermarket() {
    }
    public static Supermarket getInstance() {  
        if (uniqueInstance == null) {  
            uniqueInstance = new Supermarket();  
        }  
        return uniqueInstance;  
    }  
    // ...略
}

Lazy Initialization會在getInstance()時判斷uniqueInstance是否存在,如果不存在才建立,存在的話就直接回傳物件,如此一來就達到使用單一物件的目標了!!

但如果multi thread呼叫getInstance()會發生什麼事?我們把上述程式碼稍做修改:

public class SinglePattern {
    public static void main(String args[]) {
        Thread t1 = new FreightThread(1);  
        Thread t2 = new FreightThread(2);  
        t1.start();  
        t2.start();  
    }
}

class FreightThread extends Thread {
    private String x;

    public FreightThread(int x){
        this.x = String.valueOf(x);
    }
    public void run(){
        Supermarket mSupermarket = Supermarket.getInstance();
        Freight freight = new Freight(mSupermarket);
        freight.moveIn(30);
        System.out.println( x + "號已送達!!" + "商品數量:" + freight.mSupermarket.getQuantity());
    }
}


class Supermarket {
    private int quantity = 100;
    private static Supermarket uniqueInstance  = null;
    private Supermarket() {
    }
    public static Supermarket getInstance() {  
        if (uniqueInstance == null) {  
            uniqueInstance = new Supermarket();  
            System.out.println("建立 Supermarket");
        }  
        return uniqueInstance;  
    }  
    // ...略
}

output:

建立 Supermarket
建立 Supermarket
1號已送達!!商品數量:130
2號已送達!!商品數量:130

結果我們發現,預期的數量應該是160,送貨員並沒有把貨送到同個地點,所以結果與預期不同。為什麼會這樣呢?
thread1執行new Singleton()時,可能thread2就也執行到了new Singleton(),就會建立兩個物件,這並不是我們想要看到的結果。

class Supermarket {
    private int quantity = 100;
    private static Supermarket uniqueInstance  = null;
    private Supermarket() {
    }
    public static synchronized Supermarket getInstance() {  
        if (uniqueInstance == null) {  
            uniqueInstance = new Supermarket();  
            System.out.println("建立 Supermarket");
        }  
        return uniqueInstance;  
    }  
    // ... 略
}

output:

建立 Supermarket
2號已送達!!商品數量:130
1號已送達!!商品數量:160

於是乎我們將原本的Lazy Initialization加上了synchronized,在thread1使用getInstance時,getInstance會被lock住,這樣thread2就無法同時使用getInstance,必須等thread1結束,才可以使用,如此一來就不會產生兩個物件,也就確保超市只有一間,這樣送貨員也就可以將貨送至同一個超市。

可是這樣就會導致效能變差,而真正需要被lock的也只有new Singleton()這部分。那我們可以把synchronized往內移動,改良成效能更好的方案。

  • Double-Checked Locking / DCL

class Supermarket {

    private int quantity = 100;
    private static Supermarket uniqueInstance  = null;
    
    private Supermarket() {
    }
    
    public static Supermarket getInstance() {  
        if (uniqueInstance == null){
            synchronized(Supermarket.class){
                if(uniqueInstance == null) {
                     uniqueInstance = new Supermarket();
                     System.out.println("建立 Supermarket");
                }
            }
        }
        return uniqueInstance;  
    }  
    // ...略
}

output:

建立 Supermarket
2號已送達!!商品數量:130
1號已送達!!商品數量:160

Double-Checked Locking / DCL只在uniqueInstance不存在時才使用synchronized,這樣不管在一般情況或是多執行緒下,重複使用同一個物件,也不會影響效能。不過,這種寫法相對複雜許多。

  • Static Inner Class

Static Inner Class不但可以達到與Double-Checked Locking / DCL相同的效果,而且寫法又更加的簡潔。

class Supermarket {
    private int quantity = 100;
    private static class LazyHolder {
      private static  Supermarket uniqueInstance  = new Supermarket();
    }
    private Supermarket() {
    }
    public static  Supermarket getInstance() {
        return LazyHolder.uniqueInstance;
    }
    // ...略
}

output:

1號已送達!!商品數量:130
2號已送達!!商品數量:160

LazyHolder在程式啟動時並不會載入內部類別,只有呼叫getInstance()時才會載入進行初始化。

Static Inner Class與double-checked locking / DCL的差別在哪呢?
在C++中都沒有問題,由於java內存模型的問題(無序寫入),會在使用過程中引入錯誤:JVM在啟動對象初始化操作後,就返回了,加入此時有其他線程來請求,instance已經不為null,而初始化還未完成,如果使用,會帶來一些錯誤。

上述我們介紹了五種寫法,從最一開始建立物件的Greed Singleton、自行初始化的Lazy Initialization、為了對付thread而衍生的Lazy Initialization + synchronized、解決synchronized效能問題的Double-checked locking / DCL,以及更加精簡的Static Inner Class,這樣的順序也方便大家去記憶這五種方法,
不過下面還有一種方法,被譽為最為貼近Singleton Pattern的解決方案!!

  • Enum Singleton

enum的寫法類似於Greed Singleton,但整體來說更簡潔便利。
Effective Java作者Josh Bloch認爲enum本身的特性不僅解決多執行緒同步問題,支援序列化機制,防止反序列化重新建立新的物件,以及絕對不會建立多個物件,為實現Singleton Pattern最佳的解法。

 public class SinglePattern {
    public static void main(String args[]) {
        Supermarket mSupermarket1 = Supermarket.INSTANCE;
        Supermarket mSupermarket2 = Supermarket.INSTANCE;
        Freight freight1 = new Freight(mSupermarket1);
        Freight freight2 = new Freight(mSupermarket2);

        System.out.print("同一家超市?");

        if(mSupermarket1.equals(mSupermarket2)){
            System.out.println("yes");
        }else {
            System.out.println("no");
        }
        freight1.moveIn(30);
        System.out.println("freight1搬完後商品數量:"+freight1.mSupermarket.getQuantity());
        freight2.moveOut(50);
        System.out.println("freight2搬完後商品數量:"+freight2.mSupermarket.getQuantity());
    }
}

enum Supermarket{

    INSTANCE;
    private int quantity = 100;
    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }
    public int getQuantity() {
        return quantity;
    }
}

output:

同一家超市?yes
freight1搬完後商品數量:130
freight2搬完後商品數量:80

小結


此篇我們介紹了Singleton Pattern的概念,這邊還做個總結一下!!

  • Singleton Pattern的幾種寫法:
實現方式 優點 缺點
Greed Singleton 1. 初始化時建立,保證只有一個物件 2. Thread-safe 3. 佔用內存小 不可控制初始化時機
Enum Singleton 1. 初始化時建立,保證只有一個物件 2. Thread-safe 3. 寫法簡單 不可控制初始化時機
Lazy Initialization (Thread-unsafe) 1. 使用時才建立 2. 節省資源 Thread-unsafe
Lazy Initialization (Thread-safe) 1. 使用時才建立 2. Thread-safe 耗費過多資源(同步)
Double-checked locking / DCL 1. Thread-safe 2. 節省資源 邏輯相對複雜
Static Inner Class 1. Thread-safe 2. 節省資源 3. 寫法簡單
  • Singleton Pattern的目標:
  1. 執行一個且唯一一個Singleton物件
  2. Singleton物件可以全局使用
  • 範例程式碼


範例1:問題1
範例2:問題2
範例3:Lazy Initialization
範例4:Double-Checked Locking / DCL
範例5:Static Inner Class
範例6:Enum Singleton

  • References


設計模式學習 - Singleton Pattern
單例模式| 菜鳥教程


上一篇
[Day09] 迪米特法則 | Law of Demeter
下一篇
[Day11] 工廠模式 | Factory Pattern
系列文
從生活中認識Design Pattern30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言